Android 补丁技术学习总结(四) so修复

加载 so

  • System.loadLibrary(String soName)libs 目录下的 so 文件会被复制到应用安装目录并完成加载
  • System.load(String soPath),用于加载一个完整路径的 so 文件

注册 so

  • 静态注册,使用 Java_{类完整路径}_{方法名} 作为 native 的方法名。当 so 已经被加载之后,native 方法在第一次被执行时候就会完成注册。

    1
    2
    3
    4
    public class Test{
    public static native String test();
    }
    extern "C" jstring Java_com_effective_android_test(JNIEnv *env,jclass clazz)
  • 动态注册,借助 JNI_OnLoad 方法完成绑定。当 so 被加载时会调用 JNI_OnLoad 方法进行注册。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Test{
    public static native void testJni();
    }
    void test(JNIEnv *env,jclass clazz){
    //native impl
    }
    JNINativeMethod nativeMethods[] = {
    {"test","()V",(void *) test}
    }
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm,void *reserved){
    //...
    jclass clz = env->FindClass("com/effective/android/Test");
    if(env->RegisterNatives(clz, nativeMethods,sizeOf(nativeMethods)/sizeOf(nativeMethods[0])) != JNI_OK){
    return JNI_ERR;
    }
    //...
    }

so 热替换的限制性

  1. 针对动态注册场景
    • 对于 art 虚拟机下,可再次加载补丁 so 来完成方法映射的更新;
    • 对于 dalvik 虚拟机下,需要对补丁 so 重命名来避免来完成 art 下的方法映射的更新。
  2. 针对静态注册场景
    • 解除已经完成静态注册的方法工作难度大
    • so 中哪些静态注册的方法需要更新也很难得知

上述两个场景涉及补丁 so 的二次加载,内存损耗大,可能导致 JNI OOM出现。同时如果动态注册 so 中新增了一些方法但是对应的 dex 中没有对应的代码,则会出现 NoSuchMethodError

so 冷启动方案

假如在在应用加载 so 之前,能够先尝试加载补丁 so,再加载应用 so,就可以实现修复。自定义一个方法,替换掉 System.loadLibrary() 来完成这个逻辑。 但是存在一个缺点就是很难修复已经混淆编译的第三方库。

这里最终采取的是类似 “类修复” 的注入方案。so 库被加载之后,最终会在 DexPathList.nativeLibararyDirectories/nativeLiraryPathElements 变量所表示的目录下遍历搜索。

SDK < 23

1
2
3
4
5
6
7
8
9
10
11
private final File[] nativeLibraryDirectories;
public String findLibrary(String libraryName){
String fileName = System.mapLibraryName(libraryName);
for(File directory : nativeLibraryDirectories){
String path = new File(directory,fileName).getPath();
//如果path文件存在且可读
if(IoUtils.canOpenReadOnly(path)){
return path;
}
}
}

只需要把补丁 so 库的路径插到 nativeLibraryDirectories 最前面。

SDK >= 23

1
2
3
4
5
6
7
8
9
10
private final File[] nativeLiraryPathElements;
public String findLibrary(String libraryName){
String fileName = System.mapLibraryName(libraryName);
for(Element element : nativeLibraryElements){
String path = element.findNativeLibrary(fileName);
if(path != null){
return path;
}
}
}

只需要为补丁 so 构建一个 element 对象并插到 nativeLiraryPathElements 最前面。

但是 so 库文件存在多种 CPU 架构,补丁和 apk 一样都存在需要选择哪个 abi 的 so 来执行的问题。

Sophix 提供了一种思路, 通过从多个 abis 目录中选择一个合适的 primaryCpuAbi 目录插到 nativeLibararyDirectories/nativeLiraryPathElements 数组中。

  • SDK >= 21,直接反射拿到 ApplicationInfo 对象的 primaryCpuAbi
  • SDK < 21,由于不支持 64 位所以直接把 Build.CPU_ABI, Build.CPU_ABI2 作为 primaryCpuAbi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static{
try{
Package pm = mApp.getPackageManager();
if(pm != null){
ApplicationInfo mAppInfo = pm.getApplicationInfo(mApp.getPackageName(),0);
if(mAppInfo != null){
if(Build.VERSION>SDK_INT >= Build.VERSION_CODES>LOLLIPOP){
File thirdFiled = ApplicationInfo.class.getDeclaredFiled("primaryCpuAbi");
thirdFiled.setAccessable(true);
String cupAbi = (String) thirdFiled.get(mAppInfo);
primaryCpuAbis = new String[](cpuAbi);
}else{
primaryCpuAbis = new String[](Build.CPU_ABI,Build.CPU_ABI2);
}
}
}
}catch(Throwable t){
//...
}
}

参考资料